﻿<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>gRPC JSON transcoding</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/bootstrap.min.css" />
    <style type="text/css">
        .nav-tabs {
            margin-bottom: 1em;
        }
        .alert:empty {
            display:none;
        }
        code {
            margin-bottom: 1em;
        }
        .container {
            max-width: 720px;
        }
    </style>
</head>
<body>
    <div class="container">
        <main role="main" class="pb-3">
            <h1 class="display-4 mb-4 mt-2">gRPC JSON transcoding</h1>
            <p>
                JSON transcoding allows you call gRPC methods with familiar HTTP concepts:
            </p>
            <ul>
                <li>HTTP verbs</li>
                <li>URL parameter binding</li>
                <li>JSON requests/responses</li>
            </ul>
            <p>
                RESTful APIs for your ASP.NET Core gRPC services. No duplication!
            </p>
            <img src="/images/grpc-rest-logo.png" class="img-fluid mb-3" alt="gRPC loves REST" />
            <h5 class="mb-3">Try it now</h5>
            <ul class="nav nav-tabs" id="myTab" role="tablist">
                <li class="nav-item" role="presentation">
                    <button class="nav-link active" id="unary-tab" data-bs-toggle="tab" data-bs-target="#unary" type="button" role="tab" aria-controls="home" aria-selected="true">Basic GET</button>
                </li>
                <li class="nav-item" role="presentation">
                    <button class="nav-link" id="stream-tab" data-bs-toggle="tab" data-bs-target="#stream" type="button" role="tab" aria-controls="profile" aria-selected="false">Streaming</button>
                </li>
                <li class="nav-item" role="presentation">
                    <button class="nav-link" id="openapi-tab" data-bs-toggle="tab" data-bs-target="#openapi" type="button" role="tab" aria-controls="profile" aria-selected="false">OpenAPI</button>
                </li>
                <li class="nav-item" role="presentation">
                    <button class="nav-link" id="source-tab" data-bs-toggle="tab" data-bs-target="#source" type="button" role="tab" aria-controls="contact" aria-selected="false">View source</button>
                </li>
            </ul>
            <div class="tab-content" id="myTabContent">
                <div class="tab-pane fade show active" id="unary" role="tabpanel" aria-labelledby="unary-tab">
                    <form id="sendUnary">
                        <p>
                            Send a unary request to the Greeter gRPC service. The name is sent as a URL parameter.
                        </p>
                        <div class="row gx-2 align-items-center">
                            <div class="col-auto">
                                <label class="visually-hidden" for="inlineFormInput">Hello</label>
                                <input type="text" class="form-control mb-2" id="nameUnary" placeholder="Hello" required>
                            </div>
                            <div class="col-auto">
                                <button type="submit" class="btn btn-primary mb-2">Send</button>
                            </div>
                        </div>
                    </form>
                    <div class="alert alert-success" role="alert" id="resultUnary"></div>
                </div>
                <div class="tab-pane fade" id="stream" role="tabpanel" aria-labelledby="stream-tab">
                    <form id="sendStream">
                        <p>
                            Send a POST request to the Greeter gRPC service. JSON is sent in the request body.
                        </p>
                        <div class="row gx-2 align-items-center">
                            <div class="col-auto">
                                <label class="visually-hidden" for="inlineFormInput">Hello</label>
                                <input type="text" class="form-control mb-2" id="nameStream" placeholder="Hello" required>
                            </div>
                            <div class="col-auto">
                                <label class="visually-hidden" for="inlineFormInput">Count</label>
                                <input type="number" class="form-control mb-2" id="countStream" placeholder="Count" required>
                            </div>
                            <div class="col-auto">
                                <button type="submit" class="btn btn-primary mb-2">Send</button>
                            </div>
                        </div>
                    </form>
                    <div class="alert alert-success" role="alert" id="resultStream"></div>
                </div>
                <div class="tab-pane fade" id="openapi" role="tabpanel" aria-labelledby="openapi-tab">
                    <p>
                        View OpenAPI schema generated by Swashbuckle and send requests with Swagger UI.
                    </p>
                    <p>
                        <a href="/swagger" target="_blank">Open Swagger UI</a>
                    </p>
                </div>
                <div class="tab-pane fade" id="source" role="tabpanel" aria-labelledby="source-tab">
                    <p>
                        This website's source code on GitHub:
                    </p>
                    <ul>
                        <li><a href="https://github.com/grpc/grpc-dotnet/blob/master/examples/Transcoder/Proto/greet.proto" target="_blank">Proto/greet.proto</a> - Add <code>google.api.http</code> annotations to map HTTP calls to gRPC.</li>
                        <li><a href="https://github.com/grpc/grpc-dotnet/blob/master/examples/Transcoder/Server/Server.csproj" target="_blank">Server/Server.csproj</a> - Add <code>Microsoft.AspNetCore.Grpc.JsonTranscoding</code> NuGet package.</li>
                        <li><a href="https://github.com/grpc/grpc-dotnet/blob/master/examples/Transcoder/Server/Startup.cs" target="_blank">Server/Startup.cs</a> - Register gRPC JSON transcoding with <code>AddJsonTranscoding()</code>.</li>
                    </ul>
                    <p>
                        The service, <a href="https://github.com/grpc/grpc-dotnet/blob/master/examples/Transcoder/Server/Services/GreeterService.cs" target="_blank">Server/Services/GreeterService.cs</a>, is unchanged. gRPC and REST from one service!
                    </p>
                    <p>
                        Find out more about using gRPC JSON transcoding <a href="https://docs.microsoft.com/aspnet/core/grpc/httpapi">here</a>.
                    </p>
                </div>
            </div>
        </main>
    </div>
    <script type="text/javascript" src="/lib/bootstrap/dist/bootstrap.bundle.min.js"></script>
    <script type="text/javascript">
        var nameUnaryInput = document.getElementById('nameUnary');
        var unaryForm = document.getElementById('sendUnary');
        var resultUnaryText = document.getElementById('resultUnary');

        unaryForm.addEventListener('submit', function (event) {
            if (unaryForm.checkValidity()) {
                resultUnaryText.innerHTML = '';

                var url = '/v1/greeter/' + encodeURIComponent(nameUnaryInput.value);
                fetch(url).then(function (response) {
                    response.text().then(function (data) {
                        resultUnaryText.innerHTML = `<h6>Request</h6>
<p><code>GET ${url}</code></p>
<h6>Response</h6>
<p><code>HTTP/1.1 ${response.status} ${getReasonPhrase(response.status)}</code></p>
<p><code>${htmlEscape(data)}</code></p>
<h6>Result</h6>
<p>${JSON.parse(data).message}</p>`;
                    });
                });
            }

            event.preventDefault();
            event.stopPropagation();
        }, false);

        var nameStreamInput = document.getElementById('nameStream');
        var countStreamInput = document.getElementById('countStream');
        var streamForm = document.getElementById('sendStream');
        var resultStreamText = document.getElementById('resultStream');

        streamForm.addEventListener('submit', function (event) {
            resultStreamText.innerHTML = '';

            var url = '/v1/greeter';
            var content = JSON.stringify({ name: nameStreamInput.value, count: parseInt(countStreamInput.value) });
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: content
            }).then(async function (response) {
                var reader = response.body.getReader();
                var decoder = new TextDecoder('utf-8');
                var buffer = '';
                var values = [];

                let done, value;
                while (!done) {
                    ({ value, done } = await reader.read());
                    if (done) {
                        displayData(resultStreamText, url, content, response, values, true);
                        break;
                    }
                    buffer += decoder.decode(value, { stream: true });

                    var lines = buffer.split(/[\r\n]/);
                    buffer = lines.pop();
                    values.push(...lines);
                    displayData(resultStreamText, url, content, response, values, false);
                }
            });

            event.preventDefault();
            event.stopPropagation();
        }, false);

        function displayData(text, url, content, response, values, done) {
            text.innerHTML = `<h6>Request</h6>
<p><code>POST ${url}</code></p>
<p><code>${htmlEscape(content)}</code></p>
<h6>Response</h6>
<p><code>HTTP/1.1 ${response.status} ${getReasonPhrase(response.status)}</code></p>
<p><code>${values.map(v => htmlEscape(v) + "<br />").join("")}</code></p>`;

            if (done) {
                text.innerHTML += `<h6>Result</h6>
<ul>${values.map(v => "<li>" + JSON.parse(v).message + "</li>").join("")}</ul>`;
            }
        }

        function htmlEscape(str) {
            return String(str)
                .replace(/&/g, '&amp;')
                .replace(/'/g, '&quot;')
                .replace(/'/g, '&#39;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;');
        }

        function getReasonPhrase(statusCode) {
            switch (statusCode) {
                case (200): return "OK";
                case (201): return "Created";
                case (202): return "Accepted";
                case (203): return "Non Authoritative Information";
                case (204): return "No Content";
                case (205): return "Reset Content";
                case (206): return "Partial Content";
                case (207): return "Partial Update OK";
                case (300): return "Mutliple Choices";
                case (301): return "Moved Permanently";
                case (302): return "Moved Temporarily";
                case (303): return "See Other";
                case (304): return "Not Modified";
                case (305): return "Use Proxy";
                case (307): return "Temporary Redirect";
                case (400): return "Bad Request";
                case (401): return "Unauthorized";
                case (402): return "Payment Required";
                case (403): return "Forbidden";
                case (404): return "Not Found";
                case (405): return "Method Not Allowed";
                case (406): return "Not Acceptable";
                case (407): return "Proxy Authentication Required";
                case (408): return "Request Timeout";
                case (409): return "Conflict";
                case (410): return "Gone";
                case (411): return "Length Required";
                case (412): return "Precondition Failed";
                case (413): return "Request Entity Too Large";
                case (414): return "Request-URI Too Long";
                case (415): return "Unsupported Media Type";
                case (416): return "Requested Range Not Satisfiable";
                case (417): return "Expectation Failed";
                case (418): return "Reauthentication Required";
                case (419): return "Proxy Reauthentication Required";
                case (422): return "Unprocessable Entity";
                case (423): return "Locked";
                case (424): return "Failed Dependency";
                case (500): return "Server Error";
                case (501): return "Not Implemented";
                case (502): return "Bad Gateway";
                case (503): return "Service Unavailable";
                case (504): return "Gateway Timeout";
                case (505): return "HTTP Version Not Supported";
                case (507): return "Insufficient Storage";
                default: return "";
            }
        }
    </script>
</body>
</html>
